How to use Unions and Intersections in Typescript?

Some very useful features of TypeScript are unions and intersections.

These two functionalities allow you to manipulate existing data types using the logical AND and OR operators.

Let’s see an example. Suppose we have a video game, and in this game, we have the following code for the weapons that can be used.

export enum WeaponType {
    SHORT_RANGE,
    LONG_RANGE,
}

export type Weapon = {
    price: number,
    power: number,
    range: number,
    type: WeaponType
}

Intersections

Intersections help combine data types from different models using the “&” symbol.

Suppose we want a data type that represents something more specific, like a gun.

export type Gun = Weapon & {
    bulletCapacity: number,
    fire: () => void
}

This way, our Gun data type contains the 4 properties from Weapon, plus the 2 we specified on the right side of the symbol.

Behind the scenes, TypeScript is handling Gun like this:

type Gun = {
    price: number,
    power: number,
    range: number,
    type: WeaponType,
    bulletCapacity: number,
    fire: () => void
}

We can do the same if we want to create a data type for a sword.

export type Blade = Weapon & {
    bladeLength: number,
    brandish: () => void
}

And now we can use the previous data types for Gun and Blade to create a new data type that represents a weapon, a fusion of both: a Gunblade

export type Gunblade = Gun & Sword

As you can imagine, Gunblade contains everything that Gun had plus everything that Blade had. The default intersection omits duplicates, so you don’t have to worry about Weapon’s content being duplicated or causing errors.

type Gunblade = {
    price: number,
    power: number,
    range: number,
    type: WeaponType,
    bulletCapacity: number,
    fire: () => void,
    bladeLength: number,
    brandish: () => void
}

Unions

Unions, on the other hand, help manage multiple data types at once using the “|” symbol.

Suppose in the game, there are shops where you can sell your weapons, but for some reason, the owners are not interested in any white weapons.

Additionally, apart from the existing weapons, there’s another one to represent a bomb.

export type Bomb = Weapon & {
    explosionRadius: number,
    throw: () => void,
}

With this in mind, we can have a function that only allows you to sell certain types of weapons.

const sellWeapon = (weapon: Gun | Bomb) => {
    console.log(`Remove weapon from inventory: ${weapon} and get money equal to price`)
}

Obviously, we don’t need to implement the function for this example. The important thing here is to focus on the weapon parameter, which can accept either a gun or a bomb but not a sword in this case.

Unlike intersections, it’s uncommon to use a union in more than one place, so it’s normal to see it used without being assigned to a new data type. However, nothing prevents you from doing so if you feel it fits better with your needs or programming style.

export type SellableWeapon = Gun | Bomb
const sellWeapon = (weapon: SellableWeapon ) => {
    console.log(`Remove weapon from inventory: ${weapon} and get money equal to price`)
}

Real Use Cases

I’ve always believed that these kinds of examples help a lot in understanding theory. But to be honest, it’s hard to visualize a real-world application in web development based on these scenarios. So, I think it’s valuable to share with you the most common way I’ve applied these concepts in my day-to-day work.

Handling API Responses

As you know (and if not, now you will), it’s quite common for developers to work with an API built by someone else. This means you have no control over that code, and unfortunately, inconsistent response formats in an API are common.

That being said, here’s the procedure I usually follow:

I find all the common properties in API responses and create a data type based on that.

export type ErrorResponse = {
    errCode: number,
    date: string
}

I define type variants for the different possible responses using intersections.

export type ServerErrorResponse = ErrorResponse  & {
    error: string
}

export type ClientErrorResponse = ErrorResponse  & {
    body: {
        message: string,
        description: string
    }
}

I use unions to handle the API response call.

const handleErroResponse = (response: ServerErrorResponse | ClientErrorResponse) => {
    if (response.type === ResponseType.SERVER_ERROR) {
        openErrorModal((<ServerErrorResponse>response).error)
    } else if(response.type === ResponseType.CLIENT_ERROR) {
        openErrorModal((<ClientErrorResponse>response).body.description)
    }
}

const openErrorModal = (errorMessage: string) => {
    console.log('Open Error modal')
}

This is one of the most common use cases. And while it’s not as entertaining as the weapon example in the video game, it probably helps you better visualize where in your projects you can use both unions and intersections.